package com.example.mq.mqserver.datacenter;

import com.example.mq.common.BinaryTool;
import com.example.mq.common.MqException;
import com.example.mq.mqserver.core.MSGQueue;
import com.example.mq.mqserver.core.Message;

import java.io.*;
import java.util.LinkedList;
import java.util.Scanner;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: 言初
 * Date: 2024-09-11
 * Time: 17:19
 */
/*
    用来管理硬盘上面的消息数据
 */
public class MessageFileManager {

    // 定义一个内部类，来表示队列中对于消息的统计信息
    // 此处是一个静态内部类
    static public class Stat{
        // 消息统计文件中的存的样式是： 消息总数\t有效消息数
        public int totalCount ; // 消息的总数量
        public int validCount; // 有效消息数量
    }

    // 在硬盘中，经常会用到 获取文件路径的 情况，所以这里我们先来写一些必要的获取路径名的方法
    // 获取到指定队列对应的消息文件所在的路径
    private String getQueueDir(String queueName){
        return "./data/"+queueName;
    }

    // 获取该队列的消息数据文件路径
    // 注意：二进制文件使用txt作为后缀其实是不太合适的，txt一般表示文本，此处也就不改了，这样表示毕竟简单好理解
    // .bin   .dat
    private String getQueueDataPath(String queueName){
        return getQueueDir(queueName)+"/queue_data.txt";
    }

    // 获取队列消息统计文件的路径
    private String getQueueStatPath(String queueName){
        return getQueueDir(queueName)+"/queue_stat.txt";
    }

    // 查询队列中的消息统计文件的内容
    private Stat readStat(String queueName){
        Stat stat = new Stat();
        try(InputStream inputStream = new FileInputStream(getQueueStatPath(queueName))){
            Scanner scanner = new Scanner(inputStream);
            stat.totalCount = scanner.nextInt();
            stat.validCount = scanner.nextInt();
            return stat;
        }catch(IOException e){
            e.printStackTrace();
        }

        return null;
    }

    // 往消息统计文件中写数据，更新数据
    private void writeStat(String queueName,Stat stat){
        // 使用PrintWrite 来写文件
        // OutputStream 打开文件，默认会把原文件清空，此时写文件相当于新数据覆盖旧数据
        try(OutputStream outputStream = new FileOutputStream(getQueueStatPath(queueName))){
            PrintWriter printWriter = new PrintWriter(outputStream);
            printWriter.write(stat.totalCount+"\t"+stat.validCount);
            // 刷新操作
            printWriter.flush();
        }catch(IOException e){
            e.printStackTrace();
        }
    }

    // 创建队列对于的目录和文件
    public void createQueueFiles(String queueName) throws IOException {
        // 创建队列目录
        File baseDir = new File(getQueueDir(queueName));
        if(!baseDir.exists()){
            // 不存在就创建这个目录
            boolean ok = baseDir.mkdirs();
            if(!ok){
                // 打印出绝对路径
                throw new IOException("创建目录失败 baseDir="+baseDir.getAbsoluteFile());
            }
        }
        // 创建队列中的消息数据文件
        File queueDataFile = new File(getQueueDataPath(queueName));
        if(!queueDataFile.exists()){
            // 不存在就创建这个文件
            boolean ok = queueDataFile.createNewFile();
            if(!ok){
                System.out.println("创建文件失败 queueDataFile= "+queueDataFile.getAbsoluteFile());
            }
        }
        // 创建队列中的消息统计文件
        File queueStatFile = new File(getQueueStatPath(queueName));
        if(!queueStatFile.exists()){
            boolean ok = queueStatFile.createNewFile();
            if(!ok){
                System.out.println("创建文件失败 queueStatFile="+queueStatFile.getAbsoluteFile());
            }
        }
        // 给消息统计文件中设置初始值 0\t0
        Stat stat = new Stat();
        stat.totalCount = 0;
        stat.validCount = 0;
        writeStat(queueName,stat);
    }

    // 删除队列的目录和文件
    // 先删除文件，然后删除目录
    public void destroyQueueFiles(String queueName) throws IOException {
        File queueDataFile = new File(getQueueDataPath(queueName));
        boolean ok1= queueDataFile.delete();
        File queueStatFile = new File(getQueueStatPath(queueName));
        boolean ok2 = queueStatFile.delete();
        File baseDir = new File(getQueueDir(queueName));
        boolean ok3 = baseDir.delete();
        if(!ok1 || !ok2 ||!ok3){
            // 有一个删除失败，就算整体删除失败
            throw new IOException("删除队列目录和文件失败 baseDir= "+baseDir.getAbsolutePath());
        }
    }

    // 检查队列目录中的那2个文件是否存在
    public boolean checkFilesExits(String queueName){
        // 先判定一个队列中的消息数据文件和消息统计文件是否仍然存在
        File queueDataFile = new File(getQueueDataPath(queueName));
        if(!queueDataFile.exists()){
            // 不存在了
            return false;
        }
        File queueStatFile = new File(getQueueStatPath(queueName));
        if(!queueStatFile.exists()){
            // 不存在
            return false;
        }
        return true;

    }

    // Java标准库封装好了关于把一个int类型的4个字节的数字中这4个字节分别都拿出来的方法
    // Java标准库中的：DataOutputStream 和 DataInputStream
    // 比如：int num = 0xaabbccdd;
    // num & 0xff => dd
    // (num >> 8) & 0xff => cc
    // (num >> 16) & 0xff => bb
    // (num >> 24) & oxff => aa
    // 把一个消息放到队列的相应文件中去
    public void sendMessage(MSGQueue queue, Message message) throws MqException, IOException {
        // 检查一下队列中是否有文件
        if(!checkFilesExits(queue.getName())){
            throw  new MqException("[MessageFileManager] 队列对应文件不存在 queueName="+queue.getName());
        }
        // 把message对象进行序列化
        byte[] messageBinary = BinaryTool.toBytes(message);

        synchronized (queue){
            // 获取到当前队列消息数据文件的长度，用这个来计算出该消息的 offsetBeg 和 offsetEnd 的值
            // 把新的message对象写入到队列数据文件的末尾
            // offsetBeg = 当前文件长度+4
            // offsetEnd = 当前文件长度+4+message自身的长度
            File queueDataFile = new File(getQueueDataPath(queue.getName()));
            message.setOffsetBeg(queueDataFile.length()+4);
            message.setOffsetEnd(queueDataFile.length()+4+messageBinary.length);

            // 将消息写入到队列的消息数据文件中
            try(OutputStream outputStream = new FileOutputStream(queueDataFile,true)){
                try(DataOutputStream dataOutputStream = new DataOutputStream(outputStream)){
                    // 先将消息的长度（占4个字节写入到文件中）
                    dataOutputStream.writeInt(messageBinary.length);
                    // 写入消息本体
                    dataOutputStream.write(messageBinary);
                }
            }

            // 更新队列的消息统计文件
            Stat stat = readStat(queue.getName());
            stat.totalCount += 1;
            stat.validCount += 1;
            writeStat(queue.getName(),stat);
        }
    }

    // 删除队列中的消息，就是将这个消息读出来，反序列化回来， 然后把它的isValid参数设为0，此时这个消息就被标记为无效了
    // 按照偏移量随机访问文件中的指定位置需要用到的类是 : RandomAccessFile
    // 这个类中主要有3个方法： read  和  write ，还有一个 seek 这个方法是用来调整当前文件的光标位置的
    // 同样 在使用read 和 write 方法的时候同样也会引起光标的移动，所以记得要及时修正光标的位置
    // 逻辑删除
    public void deleteMessage(MSGQueue queue,Message message) throws IOException, ClassNotFoundException {
        synchronized (queue){
            try(RandomAccessFile randomAccessFile = new RandomAccessFile(getQueueDataPath(queue.getName()),"rw")){
                //从文件中把message数据读出来
                byte[] bufferSrc = new byte[(int)(message.getOffsetEnd()-message.getOffsetBeg())];
                randomAccessFile.seek(message.getOffsetBeg());
                randomAccessFile.read(bufferSrc);
                // 把拿到的二进制数据反序列化为message对象
                Message diskMessage =(Message) BinaryTool.fromBytes(bufferSrc);
                // 把isValid 的值设为0
                diskMessage.setIsvalid((byte)0);
                // 把数据序列化
                byte[] bufferDest = BinaryTool.toBytes(diskMessage);
                // 重新校验光标的位置
                randomAccessFile.seek(message.getOffsetBeg());
                randomAccessFile.write(bufferDest);

            }

            // 更新统计文件
            Stat stat = readStat(queue.getName());
            if(stat.validCount>0){
                stat.validCount-=1;
            }
            writeStat(queue.getName(),stat);
        }

    }

    // 从队列的消息数据文件中读出所有的消息内容，加载到内容中（放到一个链表中）
    // 这个方法是准备在程序启动的时候进行调用的，此时服务器还不能处理请求，所以不涉及多线程问题
    public LinkedList<Message> loadAllMessageFromQueue(String queueName) throws IOException, MqException, ClassNotFoundException {
        LinkedList<Message> messages = new LinkedList<>();
        try(InputStream inputStream = new FileInputStream(getQueueDataPath(queueName))){
            try(DataInputStream dataInputStream = new DataInputStream(inputStream)){
                // 使用这个变量来记录当前文件的光标所在位置
                long currentOffset = 0;
                // 循环读取一个文件
                while(true){
                    // 读取当前消息的长度，readInt方法，读到文件末尾的时候会抛出EOFException 异常
                    // 所以我们需要捕获这个异常并进行处理
                    int messageSize = dataInputStream.readInt();
                    // 按照这长度，读取消息内容
                    byte[] buffer = new byte[messageSize];
                    int actualSize = dataInputStream.read(buffer);
                    if(messageSize!=actualSize){
                        // 说明文件有问题
                        throw new MqException("[MessageFileManager] queueName="+queueName);
                    }
                    // 把读到的二进制数据 反序列化为 对象
                    Message message = (Message) BinaryTool.fromBytes(buffer);
                    // 判定是否是有效数据
                    if(message.getIsvalid()!=0x1){
                        // 无效数据,跳过，并更新currentOffset 光标在文件中位置
                        currentOffset+=(4+messageSize);
                        continue;
                    }
                    // 有效数据，把这个消息对象加到链表里面，并把这个对象的offsetBeg 和 offsetEnd 补上
                    message.setOffsetBeg(currentOffset+4);
                    message.setOffsetEnd(currentOffset+4+messageSize);
                    currentOffset+=(4+messageSize);
                    messages.add(message);

                }

            }catch(EOFException e){
                // 处理读到文件末尾时，所触发的这个异常
                System.out.println("[MessageFileManager] 恢复message 数据完成 ");
            }
        }

        return messages;

    }

    // 垃圾回收机制(复制算法），把有效数据放到一个新的文件中，然后删除旧的文件，然后把新的文件名字改一下啊
    // 记录一下垃圾回收所使用的时间来看一下用时
    // 当消息数超过2000， 无效消息超过40%的时候会自动触发垃圾回收机制

    // 判断是否触发垃圾回收
    public boolean checkGC(String queueName){
        // 判断是否触发gc
        Stat stat = readStat(queueName);
        if(stat.totalCount>2000&&(double)stat.validCount/(double)stat.totalCount<0.4){
            return true;
        }
        return false;
    }

    private String getQueueDataNewPath(String queueName){
        return getQueueDir(queueName)+"/queue_data_new.txt";
    }

    // 垃圾回收代码方法
    public void gc(MSGQueue queue) throws MqException, IOException, ClassNotFoundException {
        //进行gc的时候其他线程不能针对该队列进行任何操作，所以此处需要加锁
        synchronized (queue){
            // 记录gc耗时
            long gcBeg = System.currentTimeMillis();
            // 创建一个新的文件来存储有效消息
            File queueDataNewFile = new File(getQueueDataNewPath(queue.getName()));
            if(queueDataNewFile.exists()){
                // 正常情况下，这个文件不应该存在，所以存在就是异常
                throw  new MqException("[MessageFileManager] gc时发现该队列的queue_data_new.txt文件已经存在 queueName="+queue.getName());
            }
            boolean ok = queueDataNewFile.createNewFile();
            if(!ok){
                throw  new MqException("[MeaageFileManager] 创建文件失败 queueDataNewFile= "+queueDataNewFile.getAbsolutePath());
            }
            // 从旧文件中读取有效消息数据放到新文件中
            LinkedList<Message> messages = loadAllMessageFromQueue(queue.getName());
            // 得到有效消息的数据之后，在依次计算消息长度，并转成二进制数据，存储到新文件中去
            try(OutputStream outputStream = new FileOutputStream(queueDataNewFile)){
                try(DataOutputStream dataOutputStream = new DataOutputStream(outputStream)){
                    for(Message message : messages){
                        byte[] buffer = BinaryTool.toBytes(message);
                        // 先存入消息长度，再存入消息数据
                        dataOutputStream.writeInt(buffer.length);
                        dataOutputStream.write(buffer);
                    }
                }
            }
            // 删除旧文件，并把新文件重新命名
            File queueDataOldFile = new File(getQueueDataPath(queue.getName()));
            ok = queueDataOldFile.delete();
            if(!ok){
                throw new MqException("[MessageFileManager] 删除旧的数据文件失败 queueDataOldFile="+queueDataOldFile.getAbsolutePath());
            }
            // 给新文件重命名
            ok = queueDataNewFile.renameTo(queueDataOldFile);
            if(!ok){
                throw new MqException("[MessageFileManager] 文件重命名失败 queueDataNewFile="+queueDataNewFile.getAbsolutePath());
            }

            //更新统计文件
            Stat stat = readStat(queue.getName());
            stat.totalCount = messages.size();
            stat.validCount = messages.size();
            // 计算时间的
            long gcEnd = System.currentTimeMillis();
            System.out.println("[MessageFileManager] gc 执行完毕 queueName="+queue.getName()+",time="+
                    (gcEnd-gcBeg)+"ms");

        }
    }




}
